package org.dicr.util.jds.module;

import java.io.*;
import java.sql.*;
import java.util.*;
import java.util.prefs.*;

import org.dicr.event.*;
import org.dicr.util.jds.*;
import org.dicr.util.jds.conn.*;
import org.dicr.util.jds.conn.gui.*;

import com.borland.datastore.*;
import com.borland.dx.dataset.*;
import com.borland.dx.memorystore.*;
import com.borland.dx.sql.dataset.*;

/***********************************************************************************************************************
 * @author Igor A Tarasov me@dicr.dn.ua
 * @version 030113
 **********************************************************************************************************************/
public abstract class DicrDataModule implements DataModule, ProgramEventGenerator {
	// события
	private ProgramEventMulticaster eventMulticaster = new ProgramEventMulticaster();

	/** connection changed event */
	public static final int EVENT_CONNECTION_CHANGED = 1;

	/** Data open/close event */
	public static final int EVENT_DATAOPEN_CHANGED = 2;

	/** Cache changed event */
	public static final int EVENT_CACHE_CHANGED = 3;

	// база данных и соединение. Храним последний статус для определения
	// изменений
	private Database database = new Database();

	private ConnectionElement conn = null;

	private boolean connected = false;

	// локальный кэш
	private boolean cachEnabled = false;

	private String cachFileName = null;

	private DataStore cachStore = null;

	// обработчик ID
	private DicrChangeListener changeListener = new DicrChangeListener();

	/*******************************************************************************************************************
	 * Конструктор
	 ******************************************************************************************************************/
	public DicrDataModule() {
		try {
			jbInit();
		} catch (Exception ex) {
			DicrExceptionHandler.handleException(ex, this);
		}
	}

	/*******************************************************************************************************************
	 * Инициализация
	 ******************************************************************************************************************/
	private void jbInit() throws Exception {
		// регистрируем слушателя изменений
		this.database.addConnectionUpdateListener(new DConnListener());

		// Устанавливаем параметры соединения по умолчанию
		ConnectionsFactory factory = new ConnectionsFactory();
		Preferences pref = Preferences.userNodeForPackage(this.getClass());

		long id = pref.getLong("DefaultConnectionID", -1);
		ConnectionElement tmp = null;
		if (id != -1) tmp = factory.getConnection(id);
		if (tmp == null) tmp = factory.getDefaultConnection();
		if (tmp == null) tmp = new ConnectionElement();
		this.setConnectionElement(tmp);

		setCachFileName(pref.get("CachName", System.getProperty("user.home", System.getProperty("java.io.tmpdir", ""))
		        + File.separator + "." + this.getClass().getName() + ".cach.jds"));
	}

	/*******************************************************************************************************************
	 * Установка имени файла кэша
	 * 
	 * @param theName file name
	 ******************************************************************************************************************/
	public void setCachFileName(String theName) {
		if (theName == null) throw new NullPointerException("null cachFileName");
		String name = DataStoreUtils.checkDataFileName(theName);
		if (!name.equals(this.cachFileName)) {
			if (isCachEnabled()) { throw new IllegalStateException(
			        "Перед сменой файла кэша необходимо отключить использование кэша"); }
			try {
				this.cachFileName = name;
				Preferences pref = Preferences.userNodeForPackage(this.getClass());
				pref.put("CachFileName", name);
			} finally {
				fireCachChanged();
			}
		}
	}

	/*******************************************************************************************************************
	 * Возвращает имя файла кэша
	 * 
	 * @return file name
	 ******************************************************************************************************************/
	public String getCachFileName() {
		return this.cachFileName;
	}

	/*******************************************************************************************************************
	 * Включает/отключает кэш
	 * 
	 * @param enabled true to enable
	 * @throws Exception
	 ******************************************************************************************************************/
	public void setCachEnabled(boolean enabled) throws Exception {
		// проверяем наличие изменений
		if (this.cachEnabled == enabled) return;
		DataStore oldStore = this.cachStore;

		try {

			// создаем новый кэш
			Store newStore = null;
			if (enabled) {
				if (this.cachFileName == null) throw new IllegalStateException("Необходимо установить имя файла кэша");
				DataStore ds = DataStoreUtils.createDatabase(this.cachFileName);
				ds.open();
				newStore = ds;
			} else {
				newStore = new MemoryStore();
			}

			// устанавливаем новый кэш
			StorageDataSet[] datasets = this.getStorageDataSets();
			if (datasets != null) {
				for (int i = 0; i < datasets.length; i++) {
					StorageDataSet dataset = datasets[i];
					if (dataset != null) dataset.setStore(newStore);
				}
			}

			// сохраняем параметры нового кэша
			this.cachEnabled = enabled;
			this.cachStore = (enabled) ? (DataStore) newStore : null;

			// сохраняем свойства
			Preferences pref = Preferences.userNodeForPackage(this.getClass());
			pref.putBoolean("CachEnabled", enabled);

			// освобождаем старый кэш
			if (oldStore != null) oldStore.shutdown();
		} finally {
			// если удалось установить параметр в требуемое значение, то
			// оповещаем слушателей о смене
			if (this.cachEnabled == enabled) fireCachChanged();
		}
	}

	/*******************************************************************************************************************
	 * Возвращает признак использования локального кэша
	 * 
	 * @return true if enable
	 ******************************************************************************************************************/
	public boolean isCachEnabled() {
		return this.cachEnabled;
	}

	/*******************************************************************************************************************
	 * Оповещает слушателя о смене кэша
	 ******************************************************************************************************************/
	public void fireCachChanged() {
		this.eventMulticaster.fireProgramEvent(new ProgramEvent(this, EVENT_CACHE_CHANGED, new Object[] {
		        this.cachFileName, Boolean.valueOf(this.cachEnabled) }));
	}

	/*******************************************************************************************************************
	 * Возвращает базу данных
	 * 
	 * @return database
	 ******************************************************************************************************************/
	public Database getDatabase() {
		return this.database;
	}

	/*******************************************************************************************************************
	 * Возвращает элемент соединения
	 * 
	 * @return connection element
	 ******************************************************************************************************************/
	public ConnectionElement getConnectionElement() {
		return this.conn;
	}

	/*******************************************************************************************************************
	 * Устанавливает элемент соединения
	 * 
	 * @param connElement element to set
	 ******************************************************************************************************************/
	public void setConnectionElement(ConnectionElement connElement) {
		if (connElement == null) throw new IllegalArgumentException("null");
		if (isConnected()) throw new IllegalStateException("Необходимо отключить соединение перед его сменой");
		if (!connElement.equals(this.conn)) {
			// this.ignoreConnectionChanges = true;
			this.conn = connElement;
			this.database.setConnection(this.conn.getDescriptor());
			new ConnectionsFactory().setConnection(connElement);
			Preferences pref = Preferences.userNodeForPackage(this.getClass());
			pref.putLong("DefaultConnectionID", this.conn.getID());
			// this.ignoreConnectionChanges = false;
			fireConnectionChanged();
		}
	}

	/*******************************************************************************************************************
	 * Оповещает слушателей о смене состояния соединения
	 ******************************************************************************************************************/
	public void fireConnectionChanged() {
		this.eventMulticaster.fireProgramEvent(new ProgramEvent(this, EVENT_CONNECTION_CHANGED, new Object[] {
		        this.conn, Boolean.valueOf(this.connected) }));
	}

	/*******************************************************************************************************************
	 * Проверяет состояние соединения
	 * 
	 * @return true if connected
	 * @throws Exception
	 ******************************************************************************************************************/
	protected boolean checkConnection() throws Exception {
		boolean isConnected = !this.connected;
		try {
			if (this.database.isOpen()) {
				Connection newConn = this.database.getJdbcConnection();
				isConnected = !newConn.isClosed();
			} else isConnected = false;
		} finally {
			if (isConnected != this.connected) {
				this.connected = isConnected;
				fireConnectionChanged();
			}
		}
		return this.connected;
	}

	/*******************************************************************************************************************
	 * Возвращает признак подключения
	 * 
	 * @return true if connected
	 ******************************************************************************************************************/
	public boolean isConnected() {
		boolean isconnected = this.connected;
		try {
			isconnected = checkConnection();
		} catch (Exception ex) {
			DicrExceptionHandler.handleException(ex, this);
			isconnected = false;
		}
		return isconnected;
	}

	/*******************************************************************************************************************
	 * Открывает/закрывает подключение
	 * 
	 * @param theConnected true to connect
	 * @param promptIfNeed true to prompt
	 * @return true if operation accepted
	 * @throws Exception
	 ******************************************************************************************************************/
	public boolean setConnected(boolean theConnected, boolean promptIfNeed) throws Exception {
		if (isConnected() != theConnected) {
			try {
				if (theConnected) {
					while (!isConnected()) {
						try {
							this.database.openConnection();
						} catch (Throwable th) {
							if (!promptIfNeed) throw new Exception("Ошибка открытия базы данных", th);
							DicrExceptionHandler.handleException(th, this, true);
							ConnectionElement elem = new ConnectionsFactoryPanel(getConnectionElement()).showConnectionSelectDialog(null);
							if (elem == null) break;
							setConnectionElement(elem);
						}
					}
				} else {
					this.database.closeConnection();
				}
			} finally {
				checkConnection();
			}
		}
		return this.connected;
	}

	/*******************************************************************************************************************
	 * Создает файл базы данных
	 * 
	 * @param filename file name
	 * @throws Exception
	 ******************************************************************************************************************/
	public void createDataBase(String filename) throws Exception {
		if (filename == null) throw new NullPointerException("filename is null");
		DataStoreUtils.createDatabase(filename);
		ConnectionDescriptor desc = new ConnectionDescriptor(new DataStoreURL(true, null, filename).getURL());
		desc.setUserName(this.getClass().getName());
		desc.setPassword("password");
		desc.setDriver("com.borland.datastore.jdbc.DataStoreDriver");
		desc.setPromptPassword(false);
		ConnectionElement elem = new ConnectionElement("Соединение " + new File(filename).getName(), desc);
		new ConnectionsFactory().setConnection(elem);
		this.setConnectionElement(elem);
	}

	/*******************************************************************************************************************
	 * Возвращает все DataSet модуля
	 * 
	 * @return data sets views
	 ******************************************************************************************************************/
	public abstract DataSetView[] getDataSetViews();

	/*******************************************************************************************************************
	 * Возвращает все StorageDataSet модуля
	 * 
	 * @return return storage data sets
	 ******************************************************************************************************************/
	public abstract StorageDataSet[] getStorageDataSets();

	/*******************************************************************************************************************
	 * Возвращает все таблицы DataSetView's и StorageDataSet's
	 * 
	 * @return all datasets
	 ******************************************************************************************************************/
	protected DataSet[] getAllDataSets() {
		Vector<DataSet> alldatas = new Vector<DataSet>();
		DataSet[] dataviews = getDataSetViews();
		if (dataviews != null) {
			for (int i = 0; i < dataviews.length; i++) {
				DataSet dataview = dataviews[i];
				if (dataview == null) continue;
				if (!alldatas.contains(dataview)) alldatas.add(dataview);
				StorageDataSet storageds = dataview.getStorageDataSet();
				if (storageds == null) continue;
				if (!alldatas.contains(storageds)) alldatas.add(storageds);
			}
		}
		dataviews = getStorageDataSets();
		if (dataviews != null) {
			for (int i = 0; i < dataviews.length; i++) {
				if (dataviews[i] == null) continue;
				if (!alldatas.contains(dataviews[i])) alldatas.add(dataviews[i]);
			}
		}
		return alldatas.toArray(new DataSet[0]);
	}

	/*******************************************************************************************************************
	 * Добавляет всем DataSet слушателя редактирования для вставки rowId
	 ******************************************************************************************************************/
	protected void setRowIdListeners() {
		DataSet[] datasets = this.getDataSetViews();
		if (datasets == null) return;
		for (int i = 0; i < datasets.length; i++) {
			DataSet dataset = datasets[i];
			if (dataset == null) continue;
			dataset.removeDataChangeListener(this.changeListener);
			dataset.addDataChangeListener(this.changeListener);
		}
	}

	/*******************************************************************************************************************
	 * Устанавливает открытое/закрытое состояние всех DataSet
	 * 
	 * @param opened true to open, false to close
	 * @return errors
	 ******************************************************************************************************************/
	public List<Throwable> setDataSetsOpened(boolean opened) {
		Vector<Throwable> errors = new Vector<Throwable>();
		DataSet[] datasets = getAllDataSets();
		if (datasets.length < 1) {
			errors.add(new Exception("Нет таблиц в модуле данных"));
			return errors;
		}
		for (int i = 0; i < datasets.length; i++) {
			DataSet dataset = datasets[i];
			if (dataset == null) {
				errors.add(new Exception("null DataSet в модуле данных"));
				continue;
			}
			try {
				if (dataset.isOpen() != opened) {
					if (opened) dataset.open();
					else dataset.close();
				}
			} catch (Throwable th) {
				errors.add(th);
			}
		}
		this.eventMulticaster.fireProgramEvent(new ProgramEvent(this, EVENT_DATAOPEN_CHANGED, Boolean.valueOf(opened)));
		return errors;
	}

	/*******************************************************************************************************************
	 * Сохраняет все DataSets
	 * 
	 * @return errors
	 ******************************************************************************************************************/
	public List<Throwable> saveAllDataSets() {
		List<Throwable> errors = new ArrayList<Throwable>();
		StorageDataSet[] querys = this.getStorageDataSets();
		if (querys == null || querys.length < 1) {
			errors.add(new Exception("Нет таблиц для сохранения"));
			return errors;
		}
		if (!this.isConnected()) {
			errors.add(new Exception("Соединение не установлено."));
			return errors;
		}
		for (int i = 0; i < querys.length; i++) {
			StorageDataSet query = querys[i];
			if (query == null) {
				errors.add(new Exception("null DataSet в модуле данных"));
				continue;
			}
			try {
				if (!query.isOpen()) query.open();
				if (query.saveChangesSupported()) query.saveChanges();
			} catch (Throwable th) {
				errors.add(th);
			}
		}
		return errors;
	}

	/*******************************************************************************************************************
	 * Обновляет все DataSets
	 * 
	 * @return errors
	 ******************************************************************************************************************/
	public Vector<Throwable> refreshAllDataSets() {
		Vector<Throwable> errors = new Vector<Throwable>();
		StorageDataSet[] querys = this.getStorageDataSets();
		if (querys == null || querys.length < 1) {
			errors.add(new Exception("Нет таблиц для обновления"));
			return errors;
		}
		if (!this.isConnected()) {
			errors.add(new Exception("Соединение с базой данных не установлено."));
			return errors;
		}
		for (int i = 0; i < querys.length; i++) {
			StorageDataSet query = querys[i];
			if (query == null) {
				errors.add(new Exception("null DataSet в модуле данных"));
				continue;
			}
			try {
				if (!query.isOpen()) query.open();
				if (query.refreshSupported()) query.refresh();
			} catch (Throwable th) {
				errors.add(th);
			}
		}
		return errors;
	}

	/*******************************************************************************************************************
	 * Создает таблицу в базе данных database, согласно dataset
	 * 
	 * @param dataset dataset to create table
	 * @throws Exception
	 ******************************************************************************************************************/
	public void createTable(QueryDataSet dataset) throws Exception {
		if (dataset == null) throw new NullPointerException("null dataset");
		if (!isConnected()) throw new IllegalStateException("Не установлено соединение с базой данных");
		String query = DataStoreUtils.getCreateSQL(dataset);
		this.database.executeStatement(query);
	}

	/*******************************************************************************************************************
	 * Создает таблицы для всех QueryDataSet из модуля данных
	 * 
	 * @return errors
	 ******************************************************************************************************************/
	public Vector<Throwable> createAllTables() {
		Vector<Throwable> errors = new Vector<Throwable>();
		if (!isConnected()) {
			errors.add(new IllegalStateException("Не установлено соединение с базой данных"));
			return errors;
		}
		StorageDataSet[] storageDatas = getStorageDataSets();
		if (storageDatas == null || storageDatas.length < 1) {
			errors.add(new IllegalStateException("Нет таблиц для создания"));
			return errors;
		}
		for (int i = 0; i < storageDatas.length; i++) {
			StorageDataSet storeds = storageDatas[i];
			if (storeds == null) {
				errors.add(new NullPointerException("null dataset"));
				continue;
			}
			if (!(storeds instanceof QueryDataSet)) continue;
			QueryDataSet query = (QueryDataSet) storeds;
			try {
				createTable(query);
			} catch (Throwable th) {
				errors.add(th);
			}
		}
		return errors;
	}

	/*******************************************************************************************************************
	 * Завершение работы модуля
	 ******************************************************************************************************************/
	public void shutdown() {
		// закрываем все таблицы
		try {
			setDataSetsOpened(false);
		} catch (Throwable th) {
			DicrExceptionHandler.handleException(th, this);
		}
		// закрываем соединение
		try {
			this.setConnected(false, false);
		} catch (Throwable th) {
			DicrExceptionHandler.handleException(th, this);
		}
		// закрываем кэш
		try {
			if (this.cachStore != null) this.cachStore.shutdown();
		} catch (Exception ex) {
			DicrExceptionHandler.handleException(ex, this);
		}
	}

	/*******************************************************************************************************************
	 * Добавляет слушателя событий
	 * 
	 * @param l listener to add
	 ******************************************************************************************************************/
	@Override
    public void addProgramListener(ProgramListener l) {
		this.eventMulticaster.addProgramListener(l);
	}

	/*******************************************************************************************************************
	 * Удаляет слушателя событий
	 * 
	 * @param l listener to remove
	 ******************************************************************************************************************/
	@Override
    public void removeProgramListener(ProgramListener l) {
		this.eventMulticaster.removeProgramListener(l);
	}

	/*******************************************************************************************************************
	 * ######################################################################### Слушатель событий смены состояния
	 * соединения #########################################################################
	 * *************************************************************************
	 ******************************************************************************************************************/
	class DConnListener extends ConnectionUpdateAdapter {
		/**
		 * @see com.borland.dx.sql.dataset.ConnectionUpdateAdapter#connectionChanged(com.borland.dx.sql.dataset.ConnectionUpdateEvent)
		 */
		@Override
		public void connectionChanged(ConnectionUpdateEvent event) {
			try {
				checkConnection();
			} catch (Throwable ex) {
				DicrExceptionHandler.handleException(ex, this);
			}
		}

		/**
		 * @see com.borland.dx.sql.dataset.ConnectionUpdateAdapter#connectionClosed(com.borland.dx.sql.dataset.ConnectionUpdateEvent)
		 */
		@Override
		public void connectionClosed(ConnectionUpdateEvent event) {
			try {
				checkConnection();
			} catch (Throwable ex) {
				DicrExceptionHandler.handleException(ex, this);
			}
		}
	}

	/*******************************************************************************************************************
	 * ######################################################################### Слушатель изменения DataSet. Вставляет
	 * значение RowId #########################################################################
	 ******************************************************************************************************************/
	class DicrChangeListener extends DataChangeAdapter {
		/**
		 * @see com.borland.dx.dataset.DataChangeAdapter#dataChanged(com.borland.dx.dataset.DataChangeEvent)
		 */
		@Override
		public void dataChanged(DataChangeEvent event) {
			// проверяем тип изменения
			if (event.multiRowChange() || !(event.getID() == DataChangeEvent.ROW_ADDED)) return;

			// ищем RowID в таблице
			DataSet dataset = (DataSet) event.getSource();
			Column[] columns = dataset.getColumns();
			String colname = null;

			for (int i = 0; i < columns.length; i++) {
				Column col = columns[i];
				if (col.isRowId()) {
					colname = col.getColumnName();
					break;
				}
			}
			if (colname == null) return;

			// проверяем на необходимость заполнения
			if (!dataset.isNull(colname)) return;

			// заполняем новым значением
			long newID = System.currentTimeMillis() * 100 + new Random().nextInt(100);
			dataset.setLong(colname, newID);
		}
	}
}
